[id].vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. <template>
  2. <div class="admin--popup-form">
  3. <div v-if="isLoading" class="admin--loading">데이터를 불러오는 중...</div>
  4. <form v-else @submit.prevent="handleSubmit" class="admin--form">
  5. <!-- 형태 -->
  6. <div class="admin--form-group">
  7. <label class="admin--form-label"
  8. >형태 <span class="admin--required">*</span></label
  9. >
  10. <div class="admin--radio-group">
  11. <label class="admin--radio-label">
  12. <input v-model="formData.type" type="radio" value="html" name="type" />
  13. <span>HTML</span>
  14. </label>
  15. <label class="admin--radio-label">
  16. <input v-model="formData.type" type="radio" value="image" name="type" />
  17. <span>단일페이지</span>
  18. </label>
  19. </div>
  20. </div>
  21. <!-- 사이트 선택 -->
  22. <div class="admin--form-group">
  23. <label class="admin--form-label"
  24. >사이트 <span class="admin--required">*</span></label
  25. >
  26. <select v-model="formData.site" class="admin--form-select" required>
  27. <option value="ford">포드</option>
  28. <option value="lincoln">링컨</option>
  29. </select>
  30. </div>
  31. <!-- 제목 -->
  32. <div class="admin--form-group">
  33. <label class="admin--form-label"
  34. >제목 <span class="admin--required">*</span></label
  35. >
  36. <input
  37. v-model="formData.title"
  38. type="text"
  39. class="admin--form-input"
  40. placeholder="팝업 제목을 입력하세요"
  41. required
  42. />
  43. </div>
  44. <!-- 시작일/종료일 -->
  45. <div class="admin--form-group">
  46. <label class="admin--form-label"
  47. >시작일/종료일 <span class="admin--required">*</span></label
  48. >
  49. <div class="admin--date-range">
  50. <DatePicker
  51. v-model="formData.start_date"
  52. placeholder="시작일 선택"
  53. :max-date="formData.end_date"
  54. required
  55. />
  56. <span class="admin--date-separator">~</span>
  57. <DatePicker
  58. v-model="formData.end_date"
  59. placeholder="종료일 선택"
  60. :min-date="formData.start_date"
  61. required
  62. />
  63. </div>
  64. </div>
  65. <!-- 시작시간 (선택) -->
  66. <div class="admin--form-group">
  67. <label class="admin--form-label">시작시간 <span class="admin--hint">(미입력 시 시작일 00:00부터 바로 출력)</span></label>
  68. <div class="admin--time-group">
  69. <label class="admin--radio-label">
  70. <input v-model="formData.start_period" type="radio" value="AM" name="start_period" />
  71. <span>오전</span>
  72. </label>
  73. <label class="admin--radio-label">
  74. <input v-model="formData.start_period" type="radio" value="PM" name="start_period" />
  75. <span>오후</span>
  76. </label>
  77. <input
  78. v-model.number="formData.start_hour"
  79. type="number"
  80. class="admin--form-input admin--time-input"
  81. placeholder="시"
  82. min="1"
  83. max="12"
  84. />
  85. <span>:</span>
  86. <input
  87. v-model.number="formData.start_minute"
  88. type="number"
  89. class="admin--form-input admin--time-input"
  90. placeholder="분"
  91. min="0"
  92. max="59"
  93. />
  94. </div>
  95. </div>
  96. <!-- 종료시간 (선택) -->
  97. <div class="admin--form-group">
  98. <label class="admin--form-label">종료시간 <span class="admin--hint">(미입력 시 종료일 자정까지 노출)</span></label>
  99. <div class="admin--time-group">
  100. <label class="admin--radio-label">
  101. <input v-model="formData.end_period" type="radio" value="AM" name="end_period" />
  102. <span>오전</span>
  103. </label>
  104. <label class="admin--radio-label">
  105. <input v-model="formData.end_period" type="radio" value="PM" name="end_period" />
  106. <span>오후</span>
  107. </label>
  108. <input
  109. v-model.number="formData.end_hour"
  110. type="number"
  111. class="admin--form-input admin--time-input"
  112. placeholder="시"
  113. min="1"
  114. max="12"
  115. />
  116. <span>:</span>
  117. <input
  118. v-model.number="formData.end_minute"
  119. type="number"
  120. class="admin--form-input admin--time-input"
  121. placeholder="분"
  122. min="0"
  123. max="59"
  124. />
  125. </div>
  126. </div>
  127. <!-- 팝업창 사이즈 -->
  128. <div class="admin--form-group">
  129. <label class="admin--form-label"
  130. >팝업창 사이즈 <span class="admin--required">*</span></label
  131. >
  132. <div class="admin--size-group">
  133. <div class="admin--size-item">
  134. <label>가로</label>
  135. <input
  136. v-model.number="formData.width"
  137. type="number"
  138. class="admin--form-input"
  139. placeholder="800"
  140. min="100"
  141. required
  142. />
  143. <span>px</span>
  144. </div>
  145. <div class="admin--size-item">
  146. <label>세로</label>
  147. <input
  148. v-model.number="formData.height"
  149. type="number"
  150. class="admin--form-input"
  151. placeholder="600"
  152. min="100"
  153. required
  154. />
  155. <span>px</span>
  156. </div>
  157. </div>
  158. </div>
  159. <!-- 팝업창 위치 -->
  160. <div class="admin--form-group">
  161. <label class="admin--form-label"
  162. >팝업창 위치 <span class="admin--required">*</span></label
  163. >
  164. <div class="admin--size-group">
  165. <div class="admin--size-item">
  166. <label>TOP</label>
  167. <input
  168. v-model.number="formData.position_top"
  169. type="number"
  170. class="admin--form-input"
  171. placeholder="100"
  172. min="0"
  173. required
  174. />
  175. <span>px</span>
  176. </div>
  177. <div class="admin--size-item">
  178. <label>LEFT</label>
  179. <input
  180. v-model.number="formData.position_left"
  181. type="number"
  182. class="admin--form-input"
  183. placeholder="100"
  184. min="0"
  185. required
  186. />
  187. <span>px</span>
  188. </div>
  189. </div>
  190. </div>
  191. <!-- 쿠키설정 -->
  192. <div class="admin--form-group">
  193. <label class="admin--form-label">쿠키설정</label>
  194. <div class="admin--radio-group">
  195. <label class="admin--radio-label">
  196. <input
  197. v-model="formData.cookie_setting"
  198. type="radio"
  199. value="today"
  200. name="cookie_setting"
  201. />
  202. <span>오늘 하루 창 띄우지 않음</span>
  203. </label>
  204. <label class="admin--radio-label">
  205. <input
  206. v-model="formData.cookie_setting"
  207. type="radio"
  208. value="forever"
  209. name="cookie_setting"
  210. />
  211. <span>다시는 창을 띄우지 않음</span>
  212. </label>
  213. <label class="admin--radio-label">
  214. <input
  215. v-model="formData.cookie_setting"
  216. type="radio"
  217. value="none"
  218. name="cookie_setting"
  219. />
  220. <span>사용 안 함</span>
  221. </label>
  222. </div>
  223. </div>
  224. <!-- 출력내용 (HTML) -->
  225. <div v-if="formData.type === 'html'" class="admin--form-group">
  226. <label class="admin--form-label"
  227. >출력내용 <span class="admin--required">*</span></label
  228. >
  229. <SunEditor
  230. v-model="formData.content"
  231. height="400px"
  232. placeholder="팝업 내용을 입력하세요"
  233. />
  234. </div>
  235. <!-- 출력내용 (이미지) -->
  236. <div v-if="formData.type === 'image'" class="admin--form-group">
  237. <label class="admin--form-label"
  238. >이미지 첨부 <span class="admin--required">*</span></label
  239. >
  240. <input
  241. type="file"
  242. accept="image/*"
  243. class="admin--form-file"
  244. @change="handleImageUpload"
  245. />
  246. <div v-if="imagePreview || formData.image_url" class="admin--image-preview">
  247. <img :src="imagePreview || formData.image_url" alt="미리보기" />
  248. <button type="button" class="admin--btn-remove-image" @click="removeImage">
  249. 삭제
  250. </button>
  251. </div>
  252. </div>
  253. <!-- 링크 URL (단일페이지일 경우) -->
  254. <div v-if="formData.type === 'image'" class="admin--form-group">
  255. <label class="admin--form-label">링크 URL</label>
  256. <input
  257. v-model="formData.link_url"
  258. type="url"
  259. class="admin--form-input"
  260. placeholder="https://example.com"
  261. />
  262. </div>
  263. <!-- 링크 타겟 (단일페이지일 경우) -->
  264. <div v-if="formData.type === 'image'" class="admin--form-group">
  265. <label class="admin--form-label">링크 열기 방식</label>
  266. <div class="admin--radio-group">
  267. <label class="admin--radio-label">
  268. <input
  269. v-model="formData.link_target"
  270. type="radio"
  271. value="_blank"
  272. name="link_target"
  273. />
  274. <span>새창</span>
  275. </label>
  276. <label class="admin--radio-label">
  277. <input
  278. v-model="formData.link_target"
  279. type="radio"
  280. value="_self"
  281. name="link_target"
  282. />
  283. <span>현재창</span>
  284. </label>
  285. </div>
  286. </div>
  287. <!-- 버튼 영역 -->
  288. <div class="admin--form-actions">
  289. <button type="submit" class="admin--btn admin--btn-primary" :disabled="isSaving">
  290. {{ isSaving ? "저장 중..." : "확인" }}
  291. </button>
  292. <button type="button" class="admin--btn admin--btn-secondary" @click="goToList">
  293. 목록
  294. </button>
  295. </div>
  296. <!-- 성공/에러 메시지 -->
  297. <div v-if="successMessage" class="admin--alert admin--alert-success">
  298. {{ successMessage }}
  299. </div>
  300. <div v-if="errorMessage" class="admin--alert admin--alert-error">
  301. {{ errorMessage }}
  302. </div>
  303. </form>
  304. </div>
  305. </template>
  306. <script setup>
  307. import { ref, onMounted } from "vue";
  308. import { useRoute, useRouter } from "vue-router";
  309. import SunEditor from "~/components/admin/SunEditor.vue";
  310. import DatePicker from "~/components/admin/DatePicker.vue";
  311. definePageMeta({
  312. layout: "admin",
  313. middleware: ["auth"],
  314. });
  315. const route = useRoute();
  316. const router = useRouter();
  317. const { get, put, upload } = useApi();
  318. const { getImageUrl } = useImage();
  319. const isLoading = ref(true);
  320. const isSaving = ref(false);
  321. const successMessage = ref("");
  322. const errorMessage = ref("");
  323. const imagePreview = ref(null);
  324. const imageFile = ref(null);
  325. const formData = ref({
  326. type: "html",
  327. site: "ford",
  328. title: "",
  329. start_date: "",
  330. end_date: "",
  331. start_period: "AM",
  332. start_hour: null,
  333. start_minute: null,
  334. end_period: "AM",
  335. end_hour: null,
  336. end_minute: null,
  337. width: 800,
  338. height: 600,
  339. position_top: 100,
  340. position_left: 100,
  341. cookie_setting: "none",
  342. content: "",
  343. image_url: "",
  344. link_url: "",
  345. link_target: "_blank",
  346. });
  347. // 12시간제 ↔ 24시간 "HH:MM:SS" 변환
  348. const toTime24 = (period, hour, minute) => {
  349. if (hour === null || hour === undefined || hour === "") return null;
  350. let h = Number(hour);
  351. const m = Number(minute || 0);
  352. if (isNaN(h) || h < 1 || h > 12) return null;
  353. if (period === "PM" && h < 12) h += 12;
  354. if (period === "AM" && h === 12) h = 0;
  355. return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:00`;
  356. };
  357. const fromTime24 = (timeStr) => {
  358. if (!timeStr) return { period: "AM", hour: null, minute: null };
  359. const [hStr, mStr] = String(timeStr).split(":");
  360. let h = Number(hStr);
  361. const m = Number(mStr || 0);
  362. if (isNaN(h)) return { period: "AM", hour: null, minute: null };
  363. const period = h >= 12 ? "PM" : "AM";
  364. const hour12 = h % 12 === 0 ? 12 : h % 12;
  365. return { period, hour: hour12, minute: m };
  366. };
  367. // 데이터 로드
  368. const loadPopup = async () => {
  369. isLoading.value = true;
  370. const id = route.params.id;
  371. const { data, error } = await get(`/basic/popup/${id}`);
  372. console.log("[Popup Edit] 데이터 로드 응답:", { data, error });
  373. // API 응답: { success: true, data: {...}, message }
  374. if (data?.success && data?.data) {
  375. const popup = data.data;
  376. const startParts = fromTime24(popup.start_time);
  377. const endParts = fromTime24(popup.end_time);
  378. formData.value = {
  379. type: popup.type || "html",
  380. site: popup.site || "ford",
  381. title: popup.title || "",
  382. start_date: popup.start_date || "",
  383. end_date: popup.end_date || "",
  384. start_period: startParts.period,
  385. start_hour: startParts.hour,
  386. start_minute: startParts.minute,
  387. end_period: endParts.period,
  388. end_hour: endParts.hour,
  389. end_minute: endParts.minute,
  390. width: popup.width || 800,
  391. height: popup.height || 600,
  392. position_top: popup.position_top || 100,
  393. position_left: popup.position_left || 100,
  394. cookie_setting: popup.cookie_setting || "none",
  395. content: popup.content || "",
  396. image_url: popup.image_url || "",
  397. link_url: popup.link_url || "",
  398. link_target: popup.link_target || "_blank",
  399. };
  400. // 이미지 미리보기는 새 이미지 업로드시에만 사용
  401. // 기존 이미지는 formData.image_url을 통해 getImageUrl()로 표시
  402. imagePreview.value = null;
  403. console.log("[Popup Edit] 이미지 URL:", popup.image_url);
  404. console.log("[Popup Edit] 데이터 로드 성공:", formData.value);
  405. } else {
  406. console.log("[Popup Edit] 데이터 로드 실패");
  407. }
  408. isLoading.value = false;
  409. };
  410. // 이미지 업로드
  411. const handleImageUpload = (event) => {
  412. const file = event.target.files[0];
  413. if (!file) return;
  414. if (!file.type.startsWith("image/")) {
  415. alert("이미지 파일만 업로드 가능합니다.");
  416. return;
  417. }
  418. imageFile.value = file;
  419. // 미리보기
  420. const reader = new FileReader();
  421. reader.onload = (e) => {
  422. imagePreview.value = e.target.result;
  423. };
  424. reader.readAsDataURL(file);
  425. };
  426. // 이미지 삭제
  427. const removeImage = () => {
  428. imagePreview.value = null;
  429. imageFile.value = null;
  430. formData.value.image_url = "";
  431. };
  432. // 폼 제출
  433. const handleSubmit = async () => {
  434. successMessage.value = "";
  435. errorMessage.value = "";
  436. // 유효성 검사
  437. if (!formData.value.title) {
  438. errorMessage.value = "제목을 입력하세요.";
  439. return;
  440. }
  441. if (!formData.value.start_date || !formData.value.end_date) {
  442. errorMessage.value = "시작일과 종료일을 선택하세요.";
  443. return;
  444. }
  445. if (formData.value.type === "html" && !formData.value.content) {
  446. errorMessage.value = "출력내용을 입력하세요.";
  447. return;
  448. }
  449. if (
  450. formData.value.type === "image" &&
  451. !imageFile.value &&
  452. !formData.value.image_url
  453. ) {
  454. errorMessage.value = "이미지를 첨부하세요.";
  455. return;
  456. }
  457. isSaving.value = true;
  458. try {
  459. let imageUrl = formData.value.image_url;
  460. // 이미지 업로드 (새로운 이미지가 있는 경우)
  461. if (formData.value.type === "image" && imageFile.value) {
  462. const formDataImage = new FormData();
  463. formDataImage.append("file", imageFile.value);
  464. const { data: uploadData, error: uploadError } = await upload(
  465. "/upload/image",
  466. formDataImage
  467. );
  468. console.log("[Popup Edit] 이미지 업로드 응답:", { uploadData, uploadError });
  469. if (uploadError || !uploadData?.success) {
  470. errorMessage.value = uploadError?.message || "이미지 업로드에 실패했습니다.";
  471. isSaving.value = false;
  472. return;
  473. }
  474. imageUrl = uploadData.data?.url || uploadData.data;
  475. }
  476. // 팝업 수정
  477. const submitData = {
  478. ...formData.value,
  479. image_url: imageUrl,
  480. start_time: toTime24(formData.value.start_period, formData.value.start_hour, formData.value.start_minute),
  481. end_time: toTime24(formData.value.end_period, formData.value.end_hour, formData.value.end_minute),
  482. };
  483. const id = route.params.id;
  484. const { data, error } = await put(`/basic/popup/${id}`, submitData);
  485. if (error || !data?.success) {
  486. errorMessage.value = error?.message || data?.message || "수정에 실패했습니다.";
  487. } else {
  488. successMessage.value = data.message || "팝업이 수정되었습니다.";
  489. setTimeout(() => {
  490. router.push("/site-manager/basic/popup");
  491. }, 1000);
  492. }
  493. } catch (error) {
  494. errorMessage.value = "서버 오류가 발생했습니다.";
  495. console.error("Save error:", error);
  496. } finally {
  497. isSaving.value = false;
  498. }
  499. };
  500. // 목록으로 이동
  501. const goToList = () => {
  502. router.push("/site-manager/basic/popup");
  503. };
  504. onMounted(() => {
  505. loadPopup();
  506. });
  507. </script>